package com.google.code.lightsomy.http;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.code.lightsomy.annotations.Action;
import com.google.code.lightsomy.annotations.AfterAction;
import com.google.code.lightsomy.annotations.BeforeAction;
import com.google.code.lightsomy.annotations.Controller;
import com.google.code.lightsomy.annotations.RequestEntryFilter;
import com.google.code.lightsomy.annotations.RequestExitFilter;
import com.google.code.lightsomy.config.LightsomyConfig;
import com.google.code.lightsomy.errors.NotFound404Exception;
import com.google.code.lightsomy.route.Mapper;
import com.google.code.lightsomy.section.Section;
import com.google.code.lightsomy.util.AnnotatedClassSearch;
import com.google.code.lightsomy.util.AnnotatedMethodSearch;

/**
 * Lightsomy front controller. 
 * Delegates requests to controllers using regexp path matching.
 * 
 * TODO: url generation by route name
 * 
 * @author Viktoras Agejevas
 *
 */
public class LightsomyFrontController extends HttpServlet {

	private static final long serialVersionUID = 6660682312734256679L;

	/**
	 * Main application configuration
	 */
	private LightsomyConfig config;

	private static Log log = LogFactory.getLog(LightsomyFrontController.class);
	

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void init() throws ServletException {
		super.init();
		config = new LightsomyConfig(LightsomyConstants.CONFIG.getValue());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		control(req, resp);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		control(req, resp);

	}
	
	/**
	 * Return configured section by pathInfo.
	 * 
	 * @param pathInfo
	 * @return section for this pathInfo
	 */
	protected Section getSection(String pathInfo) {
		Section bestMatch = null;
		
		for (Section s : config.getSections()) {
			if (pathInfo.startsWith("/" + s.getName())) {
				if (bestMatch == null) {
					bestMatch = s;
				} else {
					bestMatch = bestMatch.getName().length() > s.getName().length()
								? bestMatch
								: s;
				}
			}
		}
		
		if (bestMatch != null) {
			return bestMatch;
		}
		
		for (Section s : config.getSections()) {
			if ("default".equals(s.getName())) {
				return s;
			}
		}
		
		return null;
	}
	
	/**
	 * Tries to match incoming request with some known route.
	 * If matching route is found - parses request parameters.
	 * If fails to find a matching route, <code>null</code> is returned.
	 * 
	 * @param req incoming http request
	 * @param section section for this request
	 * @return map of lightsomy request parameters, or null
	 */
	protected Map<String, String> mapRequest(HttpServletRequest req, Section section) {	
		String url = req.getRequestURL().toString();
		
		if (log.isDebugEnabled()) {
			log.debug("Mapping request for url: " + url);
		}
		
		String pathInfo = req.getPathInfo();
		
		if (section != null && !"default".equals(section.getName())) {
			pathInfo = pathInfo.substring(getSection(pathInfo).getName().length() + 1);
		}
		
		Map<String, String> params = new HashMap<String, String>();
		
		if (pathInfo == null || pathInfo.equals("") || pathInfo.equals("/")) {
			params.put(LightsomyConstants.CONTROLLER.getValue(), 
					LightsomyConstants.INDEX_CONTROLLER.getValue());
			params.put(LightsomyConstants.ACTION.getValue(), 
					LightsomyConstants.INDEX_ACTION.getValue());
		} else {
			Mapper mapper = config.getMappers().get(section);

			if (mapper.match(pathInfo)) {
				params = mapper.parse(pathInfo);
			} else {
				return null;
			}
		}
		
		if (log.isDebugEnabled()) {
			log.debug("Mapper parsed params: " + params);
		}
		
		return params;
	}

	/**
	 * Finds and instantiates controller by its short name.
	 * If it is not found by the short name - suffix "Controller" is 
	 * added and it's searched again. 
	 * If it fails to find anything - <code>null</code> is returned.
	 * 
	 * @param controller controller's short name, Index for example
	 * @return instance of controller
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 * @throws ClassNotFoundException
	 */
	protected Object findController(String pkg, String controller)
			throws InstantiationException, IllegalAccessException,
			ClassNotFoundException {
		if (controller != null) {
			Map<String, String> controllerInfo = AnnotatedClassSearch.search(pkg, Controller.class);

			String controllerClassName = null;
			if (controllerInfo.containsKey(controller)) {
				controllerClassName = controllerInfo.get(controller);
				if (log.isDebugEnabled()) {
					log.debug("Found controller for request: " + controllerClassName);
				}
				return Class.forName(controllerClassName).newInstance();
			}
			
			if (controllerInfo.containsKey(controller + "Controller")) {
				controllerClassName = controllerInfo.get(controller + "Controller");
				if (log.isDebugEnabled()) {
					log.debug("Found controller for request: " + controllerClassName);
				}
				return Class.forName(controllerClassName).newInstance();
			}
		}
		return null;
	}
	
	protected List<String> findFilters(String pkg, Class<? extends Annotation> type) {
		Map<String, String> filters = AnnotatedClassSearch.search(pkg, type);
		return new ArrayList<String>(filters.values());
	}
	
	// TODO: DRY please
	protected void runEntryFilters(List<String> names, HttpServletRequest req, 
			HttpServletResponse res) throws InstantiationException, IllegalAccessException,
			ClassNotFoundException, SecurityException, NoSuchMethodException, 
			IllegalArgumentException, InvocationTargetException {

		for (String name : names) {
			Object o = Class.forName(name).newInstance();
			RequestEntryFilter f = o.getClass().getAnnotation(RequestEntryFilter.class);
			String requestSetter = f.requestSetter();
			String responseSetter = f.responseSetter();
			String runner = f.filterMethod();
			
			Method reqSetterMethod = o.getClass().getMethod(requestSetter, HttpServletRequest.class);
			reqSetterMethod.invoke(o, req);
			Method resSetterMethod = o.getClass().getMethod(responseSetter, HttpServletResponse.class);
			resSetterMethod.invoke(o, res);
			Method runnerMethod = o.getClass().getMethod(runner);
			runnerMethod.invoke(o);
		}
	}
	
	protected void runExitFilters(List<String> names, HttpServletRequest req, 
			HttpServletResponse res) throws InstantiationException, IllegalAccessException,
			ClassNotFoundException, SecurityException, NoSuchMethodException, 
			IllegalArgumentException, InvocationTargetException {

		for (String name : names) {
			Object o = Class.forName(name).newInstance();
			RequestExitFilter f = o.getClass().getAnnotation(RequestExitFilter.class);
			String requestSetter = f.requestSetter();
			String responseSetter = f.responseSetter();
			String runner = f.filterMethod();
			
			Method reqSetterMethod = o.getClass().getMethod(requestSetter, HttpServletRequest.class);
			reqSetterMethod.invoke(o, req);
			Method resSetterMethod = o.getClass().getMethod(responseSetter, HttpServletResponse.class);
			resSetterMethod.invoke(o, res);
			Method runnerMethod = o.getClass().getMethod(runner);
			runnerMethod.invoke(o);
		}
	}
	
	protected String getServletRootUrl(HttpServletRequest req) {
		StringBuilder b = new StringBuilder();
		b.append(req.getScheme());
		b.append("://");
		b.append(req.getServerName());
		b.append(req.getServerPort() == 80 ? "" : ":" + req.getServerPort());
		b.append(req.getContextPath() == null ? "" : req.getContextPath());
		b.append(req.getServletPath() == null ? "" : req.getServletPath());
		return b.toString();
	}
	
	protected String getApplicationRootUrl(HttpServletRequest req) {
		StringBuilder b = new StringBuilder();
		b.append(req.getScheme());
		b.append("://");
		b.append(req.getServerName());
		b.append(req.getServerPort() == 80 ? "" : ":" + req.getServerPort());
		b.append(req.getContextPath() == null ? "" : req.getContextPath());
		return b.toString();
	}
	
	/**
	 * Delegates request to appropriate controller and invokes 
	 * appropriate method on clontroller (action).
	 * 
	 * @param req servlet request
	 * @param resp servlet response
	 * @throws ServletException 
	 * 
	 */
	protected void control(HttpServletRequest req, HttpServletResponse resp) throws ServletException {	
		try {
			req.getSession(true);
			addHeaders(resp);
			Section section = getSection(req.getPathInfo());
			if (log.isDebugEnabled()) {
				log.debug("Wiring request with : " + section);
			}
			Map<String, String> params = mapRequest(req, section);
			
			if (params == null) {
				throw new NotFound404Exception(req.getRequestURL().toString());
			}
					
			Object controller = findController(section.getPackageName(), 
					params.get(LightsomyConstants.CONTROLLER.getValue()));
			
			if (controller == null) {
				log.debug("Can't find " + params.get(LightsomyConstants.CONTROLLER.getValue()) + " controller");
				throw new NotFound404Exception(req.getRequestURL().toString());
			}
			
			Class<?> controllerClass = controller.getClass();
			
			Method action = getAction(
					params.get(LightsomyConstants.ACTION.getValue()), 
					controllerClass);
			
			if (action == null) {
				throw new NotFound404Exception(req.getRequestURL().toString());
			}
			
			LightsomyRequest request = new LightsomyRequest(req);
			request.putAllStringParameters(params);
			request.putParameter(
					LightsomyConstants.LIGHTSOMY_ROOT_URL.getValue(), 
					getServletRootUrl(request));
			request.putParameter(
					LightsomyConstants.LIGHTSOMY_RESOURCES_ROOT_URL.getValue(), 
					getApplicationRootUrl(request) + "/resources");
			registerTemplateLayout(controllerClass, action, request);

			if (log.isDebugEnabled()) {
				log.debug("Invoking action: " + action.getName());
			}
			
			
			runEntryFilters(findFilters(section.getPackageName(), RequestEntryFilter.class), request, resp);
			invokeAnnotatedMethod(request, resp, controller, BeforeAction.class);
			action.invoke(controller, request, resp);
			invokeAnnotatedMethod(request, resp, controller, AfterAction.class);
			runExitFilters(findFilters(section.getPackageName(), RequestExitFilter.class), request, resp);
		} catch (NotFound404Exception e) {
			resp.reset();
			resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
			throw new ServletException(e.getMessage(), e);
		} catch (Exception e) {
			try {
				resp.reset();
				resp.setContentType("text/plain; charset=UTF-8");
				e.printStackTrace(resp.getWriter());
				resp.getWriter().flush();
			} catch (IOException e1) {
				throw new ServletException(e1);
			}
		}
	}
	
	private void addHeaders(HttpServletResponse response) {
		response.setContentType("text/html; charset=UTF-8");
		response.setHeader("Cache-Control","no-cache");
		response.setHeader("Pragma","no-cache");
		response.setDateHeader ("Expires", 0);
	}

	private Method getAction(String name, Class<?> controllerClass) {
		Method action = null;

		try {
			action = controllerClass.getMethod(
					name, 
					HttpServletRequest.class, 
					HttpServletResponse.class);
		} catch (NoSuchMethodException e) {
			log.debug("Method (action): " + name + " not found");
			return null;
		}
		
		if (!action.isAnnotationPresent(Action.class)) {
			log.debug("Method (action): " + name + " is not annotated with @Action");
			return null;
		}
		
		return action;
	}

	private void registerTemplateLayout(Class<?> controllerClass,
			Method action, HttpServletRequest request) {
		String controllerLayout = 
			controllerClass.getAnnotation(Controller.class).layout();
		String actionLayout = action.getAnnotation(Action.class).layout();
		
		request.setAttribute(LightsomyConstants.LAYOUT.getValue(), 
				actionLayout.equals("") ? controllerLayout : actionLayout);
		
		if ("none".equals(actionLayout)) {
			request.setAttribute(LightsomyConstants.LAYOUT.getValue(), "");
		}
	}
	
	protected void invokeAnnotatedMethod(HttpServletRequest req,
			HttpServletResponse res, Object controller,
			Class<? extends Annotation> annotation)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException {
		if (AnnotatedMethodSearch.hasAnnotatedMethod(controller, annotation)) {
			Method m = AnnotatedMethodSearch.getAnnotatedMethod(controller,
					annotation);
			m.invoke(controller, req, res);
		}
	}
}
